﻿/////////////////////////////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2009 Eric Bowen
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 
// associated documentation files (the "Software"), to deal in the Software without restriction, including 
// without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
// copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the 
// following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial 
// portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT 
// LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
/////////////////////////////////////////////////////////////////////////////////////////////////////
// Author's blog: http://blog.scrappydog.com
// Updated source code available:  http://scrappydb.codeplex.com
/////////////////////////////////////////////////////////////////////////////////////////////////////

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Web.Caching;
using Amazon.SimpleDB.Model;
using ScrappyDB.Attributes;
using ScrappyDB.BaseClasses;
using ScrappyDB.Utilities;

namespace ScrappyDB
{
    public partial class Db
    {
        #region "Basic Query overloads with ConsistentRead defaulting to FALSE"

        public SdbCollection<T> Query<T>() where T : class, new()
        {
            return Query<T>(null, false);
        }

        public SdbCollection<T> Query<T>(string query) where T : class, new()
        {
            return Query<T>(query, null, 0, 0, false);
        }

        public SdbCollection<T> Query<T>(string query, params object[] args) where T : class, new()
        {
            var formattedQuery = string.Format(query, args);
            return Query<T>(formattedQuery, null, 0, 0, false);
        }

        public SdbCollection<T> Query<T>(string query, string nextToken) where T : class, new()
        {
            return Query<T>(query, nextToken, 0, 0, false);
        }

        public SdbCollection<T> Query<T>(string query, string nextToken, params object[] args) where T : class, new()
        {
            var formattedQuery = string.Format(query, args);
            return Query<T>(formattedQuery, nextToken, 0, 0, false);
        }

        public SdbCollection<T> Query<T>(string query, string nextToken, int startPosition, int rowsToReturn) where T : class, new()
        {
            return Query<T>(query, nextToken, startPosition, rowsToReturn, false);
        }

        public SdbCollection<T> Query<T>(string query, string nextToken, int startPosition, int rowsToReturn, params object[] args) where T : class, new()
        {
            var formattedQuery = string.Format(query, args);
            return Query<T>(formattedQuery, nextToken, startPosition, rowsToReturn, false);
        }

        #endregion

        #region "Query overloads with ConsistentRead parameter"

        public SdbCollection<T> Query<T>(bool consistentRead) where T : class, new()
        {
            return Query<T>(null, consistentRead);
        }

        public SdbCollection<T> Query<T>(string query, bool consistentRead) where T : class, new()
        {
            return Query<T>(query, null, 0, 0, consistentRead);
        }

        public SdbCollection<T> Query<T>(string query, bool consistentRead, params object[] args) where T : class, new()
        {
            var formattedQuery = string.Format(query, args);
            return Query<T>(formattedQuery, null, 0, 0, consistentRead);
        }

        public SdbCollection<T> Query<T>(string query, string nextToken, bool consistentRead) where T : class, new()
        {
            return Query<T>(query, nextToken, 0, 0, consistentRead);
        }

        public SdbCollection<T> Query<T>(string query, string nextToken, bool consistentRead, params object[] args) where T : class, new()
        {
            var formattedQuery = string.Format(query, args);
            return Query<T>(formattedQuery, nextToken, 0, 0, consistentRead);
        }

        #endregion

        /// <summary>
        /// Main SELECT Query method.  Returns collections of SimpleDbEntities.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="query"></param>
        /// <param name="nextToken"></param>
        /// <param name="startPosition"></param>
        /// <param name="rowsToReturn"></param>
        /// <param name="consistentRead"></param>
        /// <returns></returns>
        public SdbCollection<T> Query<T>(string query, string nextToken, int startPosition, int rowsToReturn, bool consistentRead) where T : class, new()
        {
            SdbCollection<T> result = null;

            //caching will break consistent reads so we need to disable it
            if (consistentRead)
                WebCache = null;

            int retryTimeout = 100;
            int maxRetry = 0;    //with queries we are only going to retry on error

            do
            {
                try
                {
                    maxRetry = 0;
                    result = QueryInternal<T>(query, nextToken, startPosition, rowsToReturn, consistentRead);
                }
                catch (Exception ex)
                {
                    if (!ex.Message.Contains("50"))
                        throw;

                    //we are expecting possible 500's and 503's here
                    Log.Error("Query<T> returned a system error:{0}", ex.Message);

                    //bump up the timeout in the event of system errors
                    maxRetry = 10000;
                }

                retryTimeout = retryTimeout * 2;

                if (retryTimeout < maxRetry)
                {
                    Log.Warn("Were are going to retry in {0} milliseconds", retryTimeout);
                    System.Threading.Thread.Sleep(retryTimeout);
                }
            } while (retryTimeout < maxRetry);

            if (result == null)
                Log.Warn("Find<T> returned null");

            return result;
        }

        private SdbCollection<T> QueryInternal<T>(string query, string nextToken, int startPosition, int rowsToReturn, bool consistentRead) where T : class, new()
        {


            Log.Debug("ScrappyDbCollection.Query Start: " + DateTime.Now.ToLongTimeString());

            try
            {

                if (startPosition > 0 && string.IsNullOrEmpty(nextToken))
                {
                    nextToken = GetStartToken<T>(query, startPosition, consistentRead);
                }

                var o = new T();

                var domains = new Domains(WebCache);
                var domainName = domains.EnsureDomain(o);

                var result = new SdbCollection<T>();
                var request = new SelectRequest()
                                  {
                                      ConsistentRead = consistentRead
                                  };

                if (!string.IsNullOrEmpty(nextToken))
                    request.NextToken = nextToken;

                string entityName = null;
                if (!(o.GetType().GetCustomAttributes(typeof(NoEntityNameFieldAttribute), true).Length > 0))
                    entityName = EntityHelper.GetEntityName(o);

                string sql;

                if (!string.IsNullOrEmpty(query) && query.Contains("select"))
                {
                    //if the query contains a "select" then use it as is
                    sql = query;

                    //TODO: validate that the query contains the right entity type
                }
                else
                {
                    //if the query doesn't contain a select than assume it is just the where clause
                    sql = "select * from " + domainName + " ";

                    if (!string.IsNullOrEmpty(entityName))
                        sql += " where EntityName = '" + entityName + "' ";

                    if (!string.IsNullOrEmpty(query) && string.IsNullOrEmpty(entityName))
                    {
                        sql += " where " + query;
                    }
                    else
                    {
                        if (!string.IsNullOrEmpty(query))
                            sql += " and " + query;
                    }
                }

                request.SelectExpression = sql;

                if (request.SelectExpression.Contains("limit"))
                {
                    if (rowsToReturn > 0)
                        throw new ArgumentException("Conflicting arguments: SQL query contains a 'limit' clause, and 'rowstoreturn' contains a value");
                    //parse rows to return
                    rowsToReturn =
                        int.Parse(request.SelectExpression.Substring(request.SelectExpression.IndexOf("limit") + 6));
                }
                else
                {
                    int rows = rowsToReturn;
                    if (rowsToReturn <= 2500)
                        rows = rowsToReturn;
                    if (rowsToReturn == 0)
                        rows = 2500;
                    if (rowsToReturn > 2500)
                        rows = 2500;
                    request.SelectExpression += " limit " + rows.ToString();
                }

                string cacheBase = Utility.GetMd5Sum(request.SelectExpression);
                string cacheKey = cacheBase;
                if (!string.IsNullOrEmpty(nextToken))
                    cacheKey += nextToken;

                if (WebCache != null)
                {
                    //this "should" be unique...
                    var c = (SdbCollection<T>)WebCache.Get(cacheKey);
                    if (c != null)
                    {
                        Log.Info("CACHE HIT: ScrappyDB.Query<T>: Domain: {0} - Entity: {1} ", domainName,
                                               EntityHelper.GetEntityName(o));
                        Log.Info("CACHE HIT: " + request.SelectExpression);

                        //pass the cache goodness on...
                        c.WebCache = WebCache;

                        return c;
                    }
                }

                Log.Info("ScrappyDB.Query<T>: Domain: {0} - Entity: {1} ", domainName,
                                       EntityHelper.GetEntityName(o));
                Log.Info(request.SelectExpression);


                SelectResponse response = Service.Select(request);

                if (response.IsSetResponseMetadata())
                    Utility.TraceResponse(response.ResponseMetadata, response.ToXML());

                if (response.SelectResult.IsSetNextToken())
                    result.NextToken = response.SelectResult.NextToken;

                if (response.IsSetSelectResult())
                {
                    SelectResult selectResult = response.SelectResult;
                    List<Item> itemList = selectResult.Item;
                    foreach (Item item in itemList)
                    {
                        var p = Find<T>(item, consistentRead);
                        result.Add(p);
                    }

                    Log.Info("{0} Rows Returned", itemList.Count.ToString());

                    if (WebCache != null)
                    {
                        WebCache.Add(cacheKey, result, null, Configuration.CacheTimeout,
                            Cache.NoSlidingExpiration,
                            CacheItemPriority.Default, null);

                        Log.Info("CACHE ADD: key - {0}", cacheKey);
                    }
                }

                //if more rows exist and rowsToReturn > our count so far... then we need to query recursively
                if (result.NextToken != null && (rowsToReturn == 0 || rowsToReturn > result.Count))
                {
                    var nextQuery = Query<T>(query, result.NextToken, startPosition, rowsToReturn, consistentRead);
                    for (int x = 0; x < nextQuery.Count; x++)
                    {
                        result.Add(nextQuery[x]);
                    }
                }

                //pass the cache goodness on...
                result.WebCache = WebCache;

                return result;
            }
            catch (Amazon.SimpleDB.AmazonSimpleDBException ex)
            {
                if (ex.Message.Contains("domain does not exist"))
                {
                    Log.Warn("WARNING: " + ex.Message);
                    Log.Warn(ex.StackTrace);

                    try
                    {
                        var o = new T();
                        string domainName = EntityHelper.GetDomainName(o);
                        Log.Warn("Attempting to create domain: {0}", domainName);

                        var domain = new Domains(WebCache);
                        domain.CreateDomain(domainName);

                        Log.Warn("Domain created");
                    }
                    catch (Exception)
                    {
                        Log.Warn("Domain creation failed");
                    }

                    //this is ok... we can just return an empty collection
                    return new SdbCollection<T>();
                }
                else
                {
                    Log.Error("EXCEPTION: " + ex.Message);
                    Log.Error(ex.StackTrace);
                    throw;
                }
            }
            catch (Exception ex)
            {
                Log.Error("EXCEPTION: " + ex.Message);
                Log.Error(ex.StackTrace);
                throw;
            }
        }


        private string GetStartToken<T>(string query, int startPosition, bool consistentRead)
       where T : class, new()
        {
            Log.Debug("DB.GetStartToken Start: " + DateTime.Now.ToLongTimeString());

            try
            {
                //caching will break consistent reads so we need to disable it
                if (consistentRead)
                    WebCache = null;

                var o = new T();
                string domainName = EntityHelper.GetDomainName(o);

                var request = new SelectRequest
                {
                    ConsistentRead = consistentRead
                };


                request.SelectExpression = "select count(*) from " + domainName + " ";

                string entityName = null;

                if (o.GetType().GetCustomAttributes(typeof(NoEntityNameFieldAttribute), true).Length == 0)
                {
                    entityName = EntityHelper.GetEntityName(o);
                    if (!string.IsNullOrEmpty(entityName))
                        request.SelectExpression += " where EntityName = '" + entityName + "' ";
                }

                if (!string.IsNullOrEmpty(query) && string.IsNullOrEmpty(entityName))
                    request.SelectExpression += " where " + query;
                else
                {
                    if (!string.IsNullOrEmpty(query))
                        request.SelectExpression += " and " + query;
                }

                if (request.SelectExpression.Contains("limit"))
                {
                    if (startPosition > 0)
                        throw new ArgumentException("Conflicting arguments: SQL query contains a 'limit' clause, and 'startPosition' contains a value");
                    //parse rows to return
                    startPosition =
                        int.Parse(request.SelectExpression.Substring(request.SelectExpression.IndexOf("limit") + 6));
                }
                else
                {
                    int rows = startPosition;
                    if (startPosition <= 2500)
                        rows = startPosition;
                    if (startPosition == 0)
                        rows = 2500;
                    if (startPosition > 2500)
                        rows = 2500;
                    request.SelectExpression += " limit " + rows.ToString();
                }

                string cacheKey = Utility.GetMd5Sum("GetStartToken" + request.SelectExpression);
                if (WebCache != null)
                {
                    //this "should" be unique...
                    object cacheResult = WebCache.Get(cacheKey);
                    if (cacheResult != null)
                    {
                        var c = (string)cacheResult;
                        Log.Info("CACHE HIT: ScrappyDB.GetStartToken<T>: Domain: {0} - Entity: {1} ", domainName,
                                 EntityHelper.GetEntityName(o));
                        Log.Info("CACHE HIT: " + request.SelectExpression);

                        return c;
                    }
                }

                Log.Info("ScrappyDB.GetStartToken<T>: Domain: {0} - Entity: {1} ", domainName,
                         EntityHelper.GetEntityName(o));
                Log.Info(request.SelectExpression);

                SelectResponse response = Service.Select(request);

                if (response.IsSetResponseMetadata())
                    Utility.TraceResponse(response.ResponseMetadata, response.ToXML());

                //TODO: need to deal with counts that take longer than 5 seconds to run
                if (!response.SelectResult.IsSetNextToken())
                {
                    Log.Warn("GetStartToken did not return a NextToken");
                    return null;
                }

                string result = response.SelectResult.NextToken;

                if (response.IsSetSelectResult())
                {
                    if (WebCache != null)
                    {
                        WebCache.Add(cacheKey, result, null, Configuration.CacheTimeout, Cache.NoSlidingExpiration,
                                     CacheItemPriority.Default, null);
                        Log.Info("CACHE ADD: key - {0}", cacheKey);
                    }
                }

                Log.Info("GetStartToken result: {0}", result);

                return result;
            }
            catch (Exception ex)
            {
                Log.Error("EXCEPTION: " + ex.Message);
                Log.Error(ex.StackTrace);
                throw;
            }
        }

        /// <summary>
        /// The query provided is expected to return a single value
        /// 
        /// Note: ConsistentRead defaults to TRUE because this method returns a single value
        /// </summary>
        /// <param name="query"></param>
        /// <returns></returns>
        public string QueryValue(string query)
        {
            return QueryValue(query, true);
        }

        /// <summary>
        /// The query provided is expected to return a single value
        /// </summary>
        /// <param name="query">A complete SimpleDB select statement</param>
        /// <param name="consistentRead"></param>
        /// <returns></returns>
        public string QueryValue(string query, bool consistentRead)
        {
            try
            {
                //caching will break consistent reads so we need to disable it
                if (consistentRead)
                    WebCache = null;

                string result = string.Empty;

                var request = new SelectRequest()
                {
                    ConsistentRead = consistentRead
                };
                request.SelectExpression = query;

                string cacheKey = Utility.GetMd5Sum(request.SelectExpression);
                if (WebCache != null)
                {
                    var c = (string)WebCache.Get(cacheKey);
                    if (c != null)
                    {
                        Log.Info("CACHE HIT: ScrappyDB.QueryValue:  {0}", query);
                        Log.Info("CACHE HIT: " + request.SelectExpression);

                        return c;
                    }
                }


                Log.Info("ScrappyDB.QueryValue:  {0}", query);
                Log.Info(request.SelectExpression);

                SelectResponse response = Service.Select(request);

                if (response.IsSetResponseMetadata())
                    Utility.TraceResponse(response.ResponseMetadata, response.ToXML());

                //TODO: need to deal with querys that take longer than 5 seconds to run
                if (response.SelectResult.IsSetNextToken())
                    Log.Info("Query returned NextToken and may have incomplete results");

                //    result.NextToken = response.SelectResult.NextToken;

                if (response.IsSetSelectResult())
                {
                    SelectResult selectResult = response.SelectResult;

                    if (selectResult != null && selectResult.Item != null && selectResult.Item.Count > 0)
                    {
                        result = selectResult.Item[0].Attribute[0].Value;
                    }

                    if (WebCache != null)
                    {
                        WebCache.Add(cacheKey, result, null, Configuration.CacheTimeout,
                            Cache.NoSlidingExpiration,
                            CacheItemPriority.Default, null);
                        Log.Info("CACHE ADD: ScrappyDB.QueryValue:  {0}", query);
                    }
                }

                return result;
            }
            catch (Amazon.SimpleDB.AmazonSimpleDBException ex)
            {
                if (ex.Message.Contains("domain does not exist"))
                {
                    Log.Warn("WARNING: " + ex.Message);
                    Log.Warn(ex.StackTrace);

                    //this is ok... we can just return null
                    return null;
                }

                Log.Error("EXCEPTION: " + ex.Message);
                Log.Error(ex.StackTrace);
                throw;
            }
            catch (Exception ex)
            {
                Log.Error("EXCEPTION: " + ex.Message);
                Log.Error(ex.StackTrace);
                throw;
            }
        }

        public Collection<T> QueryByExample<T>(T o) where T : class, new()
        {
            throw new NotImplementedException("QueryByExample<T>");
        }

        //public List[] GetRelatedItemNames(string domainName, string parentItemProperty, string parentItemValue)
        //{
        //    var result = new ArrayList[1];

        //    SelectRequest request = new SelectRequest()
        //{
        //                              ConsistentRead = consistentRead
        //                          };

        //    request.SelectExpression = "select * from " + domainName + " where EntityName = '" + domainName + "'";

        //    request.SelectExpression += " and " + parentItemProperty + " = " + parentItemValue;

        //    SelectResponse response = Service.Select(request);

        //    Log.Info(response.ToXML());

        //    if (response.IsSetSelectResult())
        //    {
        //        //QueryWithAttributesResult queryWithAttributesResult = response.QueryWithAttributesResult;
        //        //List<Item> itemList = queryWithAttributesResult.Item;
        //        //foreach (var item in itemList)

        //        SelectResult selectResult = response.SelectResult;
        //        List<Item> itemList = selectResult.Item;
        //        foreach (Item item in itemList)
        //        {
        //            //
        //        }
        //    }

        //    if (response.IsSetResponseMetadata())
        //    {
        //        Log.Info("            ResponseMetadata");
        //        ResponseMetadata responseMetadata = response.ResponseMetadata;
        //        if (responseMetadata.IsSetRequestId())
        //        {
        //            Log.Info("                RequestId");
        //            Log.Info("                    {0}", responseMetadata.RequestId);
        //        }
        //        if (responseMetadata.IsSetBoxUsage())
        //        {
        //            Log.Info("                BoxUsage");
        //            Log.Info("                    {0}", responseMetadata.BoxUsage);
        //        }
        //    }

        //    return result;
        //}
    }
}